Pass by Value vs CompositionLocal vs Static CompositionLocal|546

들어가며

1. CompositionLocal이란?

컴포저블 트리 전체에 데이터를 암시적으로 전달하는 메커니즘이다.

  • 일반적인 방법
    • 부모 자식으로 파라미터를 전달하는 prop drilling 방식을 활용한다.
  • CompositionLocal
    • 특정 범위 scope에 값을 설정하고 하위 어디서든 직접 접근이 가능하게 한다.
    • 따라서 색상, 폰트 크기와 같이 자주 접근해야 하는 곳에서 활용된다.

2. 어떤 문제에서 등장하게 되었는가?

2-1. Props Drilling 문제점

상위에서 하위로 파라미터를 전달하여 중간 컴포넌트는 불필요하게 값을 받아야 한다. 또한 Props Drilling 방식 고유의 하위 컴포넌트가 많아질 경우 구조가 복잡해지며 가독성이 떨어진다는 문제점이 있었다.

3. 동작 구조

1단계 Local 정의

val LocalTheme = compositionLocalOf { DefaultTheme }

2단계 값 제공

CompositionLocalProvider(
	LocalTheme provides theme
){
	Content()
}

3단계 값 사용

val theme = LocalTheme.current

연습용으로 만든 UI 시스템 CompositionLocal을 활용한 코드

data class RunningTrackerColors(  
    val primary: Color,  
    val onPrimary: Color,  
    val background: Color,  
    val onBackground: Color,  
    val surface: Color,  
    val onSurface: Color,  
    val secondaryText: Color,  
    val accent: Color,  
    val error: Color,  
    val success: Color,  
    val warning: Color,  
)  
  
val DarkColorPalette = RunningTrackerColors(  
    primary = Color(0xFF00E5FF),  
    onPrimary = Color(0xFF000000),  
    background = Color(0xFF0F172A),  
    onBackground = Color(0xFFF8FAFC),  
    surface = Color(0xFF1E293B), // Navy/Grey Surface  
    onSurface = Color(0xFFF1F5F9),  
    secondaryText = Color(0xFF94A3B8),  
    accent = Color(0xFFFF4081),  
    error = Color(0xFFEF4444),  
    success = Color(0xFF10B981), // Emerald Green  
    warning = Color(0xFFF59E0B),  
)  
  
val LocalRunningTrackerColors = staticCompositionLocalOf {
    DarkColorPalette  
}
@Composable  
fun RunningTrackerTheme(  
    colors: RunningTrackerColors = DarkColorPalette,  
    typography: RunningTrackerTypography = DefaultTypography,  
    spacing: RunningTrackerSpacing = RunningTrackerSpacing(),  
    content: @Composable () -> Unit  
) {  
    CompositionLocalProvider(  
        LocalRunningTrackerColors provides colors,  
        LocalRunningTrackerTypography provides typography,  
        LocalRunningTrackerSpacing provides spacing,  
    ) {  
        content()  
    }  
  
}  
  
object AppTheme {  
    val colors: RunningTrackerColors  
        @Composable  
        get() = LocalRunningTrackerColors.current  
  
    val typography: RunningTrackerTypography  
        @Composable  
        get() = LocalRunningTrackerTypography.current  
  
    val spacing: RunningTrackerSpacing  
        @Composable  
        get() = LocalRunningTrackerSpacing.current  
}
data class RunningTrackerSpacing (  
    val default: Dp = 0.dp,  
    val tiny: Dp = 4.dp,  
    val small: Dp = 8.dp,  
    val normal: Dp = 16.dp,  
    val large: Dp = 24.dp,  
    val extraLarge: Dp = 32.dp  
)  
  
val LocalRunningTrackerSpacing = staticCompositionLocalOf {  
    RunningTrackerSpacing()  
}
data class RunningTrackerTypography(  
    val h1: TextStyle,  
    val h2: TextStyle,  
    val h3: TextStyle,  
    val body1: TextStyle,  
    val body2: TextStyle,  
    val caption: TextStyle,  
    val button: TextStyle,  
)  
  
val DefaultTypography = RunningTrackerTypography(  
    h1 = TextStyle(  
        fontFamily = FontFamily.Default,  
        fontWeight = FontWeight.Bold,  
        fontSize = 32.sp  
    ),  
    h2 = TextStyle(  
        fontFamily = FontFamily.Default,  
        fontWeight = FontWeight.Bold,  
        fontSize = 24.sp  
    ),  
    h3 = TextStyle(  
        fontFamily = FontFamily.Default,  
        fontWeight = FontWeight.SemiBold,  
        fontSize = 20.sp  
    ),  
    body1 = TextStyle(  
        fontFamily = FontFamily.Default,  
        fontWeight = FontWeight.Normal,  
        fontSize = 16.sp,  
        lineHeight = 24.sp  
    ),  
    body2 = TextStyle(  
        fontFamily = FontFamily.Default,  
        fontWeight = FontWeight.Normal,  
        fontSize = 14.sp,  
        lineHeight = 20.sp  
    ),  
    caption = TextStyle(  
        fontFamily = FontFamily.Default,  
        fontWeight = FontWeight.Normal,  
        fontSize = 12.sp,  
    ),  
    button = TextStyle(  
        fontFamily = FontFamily.Default,  
        fontWeight = FontWeight.Bold,  
        fontSize = 16.sp,  
    ),  
)  
  
val LocalRunningTrackerTypography = staticCompositionLocalOf {  
    DefaultTypography  
}

4. 주로 사용되는 사례

1 ) 디자인 시스템

  • 색상, typography, shape
  • MaterialTheme 내부 구현이 CompositionLocal 기반

2 ) 전역 설정값

  • locale
  • density
  • context

3 ) 크로스 커팅 데이터

  • 채팅 테마
  • 앱 전체 설정

5. CompositonLocal 단점

1. 디버깅 어려움

  • 값이 어디서 오는지 추적하기 어려움

2. 의존성 숨겨짐

  • 코드만 보고 이해하기 힘듦

3. 남용 위험

  • 전역 변수처럼 사용할 경우 설계가 복잡해짐.